Extensions

Overview

DataObjects.Net Extensions are small projects that extend standard functionality of DataObjects.Net core. They are maintained by Xtensive engineers and volunteers from DataObjects.Net community. Each extension has a corresponding NuGet package so they can be installed separately or in any combination. Using NuGet is a recommended way of installing the extensions.

_images/NuGetPackages.png

The extensions are published on CodePlex with source code open.

Bulk Operations

The extension provides a set of IQueryable extension methods that are translated to server-side UPDATE or DELETE commands.

How to use

Add Bulk Operations package to your project.

Demo

  1. Update primitive property with a constant value, e.g.:
Query.All<Bar>()
  .Where(a => a.Id == 1)
  .Set(a => a.Count, 2)
  .Update();
  1. Updating persistent property with expression, computed on server, e.g.:
Query.All<Bar>()
  .Where(a => a.Id==1)
  .Set(a => a.Count, a => a.Description.Length)
  .Update();
  1. Setting a reference to an entity that is already loaded into current Session
// Emulating entity loading
var bar = Query.Single<Bar>(1);

Query.All<Foo>()
  .Where(a => a.Id == 2)
  .Set(a => a.Bar, bar)
  .Update();
  1. Setting a reference to an entity that is not loaded into Session, 1-st way
Query.All<Foo>()
  .Where(a => a.Id == 1)
  .Set(a => a.Bar, a => Query.Single<Bar>(1))
  .Update();
  1. Setting a reference to an entity that is not loaded into Session, 2-nd way
Query.All<Foo>()
  .Where(a => a.Id == 1)
  .Set(a => a.Bar, a => Query.All<Bar>().Single(b => b.Name == "test"))
  .Update();
  1. Constructing update expressions of the fly
bool condition = CheckCondition();
var query = Query.All()<Bar>
  .Where(a => a.Id == 1)
  .Set(a => a.Count, 2);

if(condition)
  query = query.Set(a => a.Name, a => a.Name + "test");
query.Update();
  1. Updating lots of properties at once
Query.All<Bar>()
  .Where(a => a.Id == 1)
  .Update(a => new Bar(null) { Count = 2, Name = a.Name + "test", dozens of other properties... });
  1. Deleting entities
Query.All<Foo>()
  .Where(a => a.Id == 1)
  .Delete();

Localization/Internationalization

The extension transparently solves a task of application or service localization. This implies that localizable resources are a part of domain model so they are stored in database.

How to use

  1. Add Localization package to your project.
  2. Include types from Xtensive.Orm.Localization assembly into the domain:
<Xtensive.Orm>
  <domains>
    <domain ... >
      <types>
        <add assembly="your assembly"/>
        <add assembly="Xtensive.Orm.Localization"/>
      </types>
    </domain>
  </domains>
</Xtensive.Orm>
  1. Implement ILocalizable<TLocalization> on your localizable entities, e.g.:
[HierarchyRoot]
public class Page : Entity, ILocalizable<PageLocalization>
{
  [Field, Key]
  public int Id { get; private set; }

  // Localizable field. Note that it is non-persistent
  public string Title
  {
    get { return Localizations.Current.Title; }
    set { Localizations.Current.Title = value; }
  }

  [Field] // This is a storage of all localizations for Page class
  public LocalizationSet<PageLocalization> Localizations { get; private set; }

  public Page(Session session) : base(session) {}
}
  1. Define corresponding localizations, e.g.:
[HierarchyRoot]
public class PageLocalization : Localization<Page>
{
  [Field(Length = 100)]
  public string Title { get; set; }

  public PageLocalization(Session session, CultureInfo culture, Page target)
    : base(session, culture, target) {}
}

Demo

  1. Access localizable properties as regular ones, e.g.:
page.Title = "Welcome";
string title = page.Title;
  1. Mass editing of localizable properties:
var en = new CultureInfo("en-US");
var sp = new CultureInfo("es-ES");
var page = new Page(session);
page.Localizations[en].Title = "Welcome";
page.Localizations[sp].Title = "Bienvenido";
  1. Value of localizable properties reflects culture of the current Thread, e.g.:
Thread.CurrentThread.CurrentCulture = new CultureInfo("en-US");
string title = page.Title; // title is "Welcome"

Thread.CurrentThread.CurrentCulture = new CultureInfo("es-ES");
string title = page.Title; // title is "Bienvenido"
  1. Instead of altering CurrentThread, instance of LocalizationScope can be used, e.g.:
using (new LocalizationScope(new CultureInfo("en-US"))) {
  string title = page.Title; // title is "Welcome"
}

using (new LocalizationScope(new CultureInfo("es-ES"))) {
  string title = page.Title; // title is "Bienvenido"
}
  1. LINQ queries that include localizable properties are transparently translated
Thread.CurrentThread.CurrentCulture = new CultureInfo("en-US");
var query = from p in session.Query.All<Page>()
  where p.Title=="Welcome"
  select p;
Assert.AreEqual(1, query.Count());

Thread.CurrentThread.CurrentCulture = new CultureInfo("es-ES");
var query = from p in session.Query.All<Page>()
  where p.Title=="Bienvenido"
  select p;
Assert.AreEqual(1, query.Count());

Reprocessing

The extension provides API for reprocessible operations. The reprocessible operation should represent a separate block of logic, usually a delegate of a method and be transactional.

How to use

  1. Add Reprocessing package to your project.

Demo

  1. Simple reprocessible operation looks like this:
Domain.Execute(session =>
  {
    // Task logic
  });

Session is provided by reprocessing infrastructure. No session activation perfromed so use the instance you were gived by delegate.

2. Use WithSession to pass session from ouside, it this case infrastructure won’t open any session and uses yours instead. Don’t use session activation to pass session to delegate.

using (var externalSession = Domain.OpenSession()) {
  Domain.WithSession(externalSession)
    .Execute(session =>
      {
        // Task logic
      });
}
  1. There are 3 strategies that can be used for task execution:
  • HandleReprocessibleException strategy
    The strategy catches all reprocessible expections (deadlock and transaction serialization exceptions) and makes another attempt to execute the task
  • HandleUniqueConstraintViolation strategy
    The same as previous one but also catches unique constraint violation exception
  • NoReprocess strategy
    No reprocessing is provided

To indicate that a particular strategy should be used, use the following syntax:

Domain.WithStrategy(new HandleReprocessExceptionStrategy())
  .Execute(session =>
    {
      // Task logic
    });

4. To omit setting up the strategy each time consider configuring it in application configuration file, e.g.:

<configSections>
  ...
  <section name="Xtensive.Orm.Reprocessing"
    type="Xtensive.Orm.Reprocessing.Configuration.ConfigurationSection, Xtensive.Orm.Reprocessing" />
</configSections>

<Xtensive.Orm.Reprocessing
  defaultTransactionOpenMode="New"
  defaultExecuteStrategy="Xtensive.Orm.Reprocessing.HandleReprocessableExceptionStrategy, Xtensive.Orm.Reprocessing">
</Xtensive.Orm.Reprocessing>

Having that done, in scenarios with no strategy specified, the extension will automatically use the strategy from the configuration.

Security

The extension provides security layer (authentication services, principals, roles, secured queries) There are 2 main parts that can also be used separately: authentication services and role-based access to domain entities

How to use

  1. Add Security package to your project.
  2. Include types from Xtensive.Orm.Security assembly into the domain:
<Xtensive.Orm>
  <domains>
    <domain ... >
      <types>
        <add assembly="your assembly"/>
        <add assembly="Xtensive.Orm.Security"/>
      </types>
    </domain>
  </domains>
</Xtensive.Orm>
  1. If you are planning to use one of authentication services add
<section name="Xtensive.Orm.Security" type="Xtensive.Orm.Security.Configuration.ConfigurationSection,
Xtensive.Orm.Security" />

and set up the desired hashing service:

<Xtensive.Orm.Security>
  <hashingService name="plain"/>
  <!-- other options are: md5, sha1, sha256, sha384, sha512 -->
</Xtensive.Orm.Security>

Demo

  1. Define a class that inherits abstract GenericPrincipal class that will describe your users, e.g.:
[HierarchyRoot]
public class User : GenericPrincipal
{
  [Field, Key]
  public int Id { get; private set; }

  [Field]
  public string LastName { get; set; }

  [Field]
  public string FirstName { get; set; }

  ...

  public User(Session session) : base(session) {}
}
  1. Having the User class defined, it can be used for user creation and authentication.
// Creating a user
using (var session = Domain.OpenSession()) {
  using (var transaction = session.OpenTransaction()) {
    var user = new User(session);
    user.Name = "admin";
    user.SetPassword("password");
    transaction.Complete();
  }
}

// Authenticating a user
using (var session = Domain.OpenSession()) {
  using (var transaction = session.OpenTransaction()) {
    var user = session.Authenticate("admin", "password");
    transaction.Complete();
  }
}
  1. Define a hierarchy of roles. A role is a set of permissions for a job function within a company, e.g.:
EmployeeRole
|
|- StockManagerRole
|
|- SalesRepresentativeRole
   |
   |- SalesManagerRole
   |
   |- SalesPresidentRole


// This is base role for all employees
[HierarchyRoot(InheritanceSchema = InheritanceSchema.SingleTable)]
public abstract class EmployeeRole : Role
{
  [Field, Key]
  public int Id { get; set; }

  protected override void RegisterPermissions()
  {
    // All employees can read products
    RegisterPermission(new Permission<Product>());
    // All employees can read other employees
    RegisterPermission(new Permission<Employee>());
  }

  protected EmployeeRole(Session session)
    : base(session) {}
}

public class StockManagerRole : EmployeeRole
{
  protected override void RegisterPermissions()
  {
    // Stock manager inherits Employee permissions
    base.RegisterPermissions();

    // Stock manager can read and write products
    RegisterPermission(new Permission<Product>(canWrite:true));
  }

  public StockManagerRole(Session session)
    : base(session) {}
}

// Create instances of roles on first domain initialization
using (var session = Domain.OpenSession()) {
  using (var transaction = session.OpenTransaction()) {
    new SalesRepresentativeRole(session);
    new SalesManagerRole(session);
    new SalesPresidentRole(session);
    new StockManagerRole(session);
    transaction.Complete();
  }
}
  1. Members of staff are assigned particular roles, e.g.:
using (var session = Domain.OpenSession()) {
  using (var transaction = session.OpenTransaction()) {
    var stockManagerRole = session.Query.All<StockManagerRole>().Single();
    var user = new User(session);
    user.Name = "peter";
    user.SetPassword("password");
    user.Roles.Add(stockManagerRole);
    transaction.Complete();
  }
}
  1. Checking whether a user has the required role
user.IsInRole("StockManagerRole");
// or
user.Roles.Contains(stockManagerRole);
  1. Session impersonation
using (var imContext = session.Impersonate(user)) {
  // inside the region the session is impersonated with the specified
  // principal and set of their roles and permissions

  // Checking whether the user has a permission for reading Customer entities
  imContext.Permissions.Contains<Permission<Customer>>(p => p.CanRead);

  // Checking whether the user has a permission for writing to Customer entities
  imContext.Permissions.Contains<Permission<Customer>>(p => p.CanWrite);

  // another way
  var p = imContext.Permissions.Get<Permission<Customer>>();
  if (p != null && p.CanRead)
    // allow doing some stuff
}

To end the impersonation call ImpersonationContext.Undo() or Dispose() method.

Impersonation contexts can be nested, e.g.:

using (var userContext = session.Impersonate(user)) {
  // do some user-related stuff

  using (var adminContext = session.Impersonate(admin)) {
    // do some admin stuff
  }

  // we are still in user impersonation context
}
// no context here

7. Secure (restrictive) queries
A role may set up a condition that will be automatically added to any query and filters the query results, e.g.:

public class AutomobileManagerRole : EmployeeRole
{
  private static IQueryable<Customer> GetCustomers(ImpersonationContext context, QueryEndpoint query)
  {
    return query.All<Customer>()
      .Where(customer => customer.IsAutomobileIndustry);
  }

  protected override void RegisterPermissions()
  {
    base.RegisterPermissions();
    // This permission tells that a principal can read/write customers
    // but only those that are returned by the specified condition
    RegisterPermission(new CustomerPermission(true, GetCustomers));
  }

  public AutomobileManagerRole(Session session)
    : base(session) {}
}

Now all employees that have AutomobileManagerRole will read customers that have IsAutomobileIndustry property set to true, e.g.:

using (var session = Domain.OpenSession()) {
  using (var transaction = session.OpenTransaction()) {
    var automobileManagerRole = session.Query.All<AutomobileManagerRole>().Single();
    var user = new User(session);
    user.Name = "peter";
    user.SetPassword("password");
    user.Roles.Add(automobileManagerRole);

    using (var context = session.Impersonate(user)) {
      var customers = Query.All<Customer>();
      // Inside the impersonation context the above-mentioned query condition
      // will be added automatically so user will get only automobile customers
    }
    transaction.Complete();
  }
}

Change Tracking/Auditing

The extension provides tracking/auditing funtionality on Session/Domain level.

How to use

  1. Add Tracking package to your project.
  2. Include types from Xtensive.Orm.Tracking assembly into the domain:
<Xtensive.Orm>
  <domains>
    <domain ... >
      <types>
        <add assembly="your assembly"/>
        <add assembly="Xtensive.Orm.Tracking"/>
      </types>
    </domain>
  </domains>
</Xtensive.Orm>
  1. To track changes on Session level obtain an instance of ISessionTrackingMonitor through Session.Services.Get<ISessionTrackingMonitor>() method. To track changes on Domain level (from all sessions) obtain an instance of IDomainTrackingMonitor through Domain.Services.Get<IDomainTrackingMonitor>() method.

4. Subscribe to TrackingCompleted event. After each tracked transaction is committed you receive the TrackingCompletedEventArgs object.

5. TrackingCompletedEventArgs.Changes contains a collection of ITrackingItem objects, each of them represents a set of changes that occurred to an Entity within the transaction committed.

Demo

  1. Subscribe to ISessionTrackingMonitor/IDomainTrackingMonitor TrackingCompleted event
var monitor = Domain.Services.Get<IDomainTrackingMonitor>();
monitor.TrackingCompleted += TrackingCompletedListener;
  1. Do some changes to persistent entities
using (var session = Domain.OpenSession()) {
 using (var t = session.OpenTransaction()) {
   var e = new MyEntity(session);
   e.Text = "some text";
   t.Complete();
 }
}
  1. Handle TrackingCompleted event call and do whatever you want with tracked changes.
private void TrackingCompletedListener(object sender, TrackingCompletedEventArgs e)
{
 foreach (var change in e.Changes) {
   Console.WriteLine(change.Key);
   Console.WriteLine(change.State);

   foreach (var value in change.ChangedValues) {
     Console.WriteLine(value.Field.Name);
     Console.WriteLine(value.OriginalValue);
     Console.WriteLine(value.NewValue);
   }
 }
}

Web

The extension adds integration for DataObjects.Net and ASP.NET. It contains SessionManager class which is an implementation of IHttpModule and automatically provides Session and transaction for each web request.

SessionManager has the following features:

  1. When Session.Current is being accessed, and there is no current Session, it will provide a new instance of Session. In this case a new transaction will be created. It will be committed on successful completion of http request, otherwise it will be rolled back.
  2. Setting SessionManager.Demand().Error to true will lead to rollback of this transaction.
  3. SessionManager.Current (and SessionManager.Demand()) returns the instance of SessionManager bound to the current HttpContext, i.e. current SessionManager. Its Session property (if not null) is the same value as the one provided by Session.Current.

Note that presence of SessionManager does not prevent you from creating Sessions manually. It operates relying on Session.Resolver event, which is raised only when there is no current Session.

Finally, no automatic Session + transaction will be provided, if you don’t use Session.Current/Session.Demand() methods in your code (directly or indirectly). So e.g. requests to static web pages won’t lead to any DB interaction.

How to use

  1. Add Web package to your project.
  2. Register SessionManager in HttpModulesSection in web.config file.

web.config:

<configuration>
  <system.web>
    <httpModules>
      <add name="SessionManager" type="Xtensive.Orm.Web.SessionManager, Xtensive.Orm.Web"/>
    </httpModules>
  </system.web>
</configuration>
  1. Set up SessionManager.DomainBuilder property in Application_Start method of your Global.asax.cs file.
using Xtensive.Orm.Web;

public class Global : System.Web.HttpApplication
{
  protected void Application_Start(object sender, EventArgs e)
  {
    SessionManager.DomainBuilder = DomainBuilder.Build;
  }
}

Demo

public partial class EditCustomer : System.Web.UI.Page
{
  protected void Page_Load(object sender, EventArgs e)
  {
    // Session is provided automatically, transaction also starts
    var session = Session.Demand();
    var id = Request["customerId"];
    if (!string.IsNullOrEmpty(id)) {
      var customerId = int.Parse(id);
      var customer = session.Query.Single<Customer>(customerId);
    }
    ...
  }

  protected void Save(object sender, EventArgs e)
  {
    try {
      var session = Session.Demand();
      if (customer==null)
        customer = new Customer(session);
      customer.Name = textName.Text;
      ...
      Back();
    }
    catch(InvalidOperationException exception) {
      // This will roll back the transaction on end of http request
      SessionManager.Current.HasErrors = true;
    }
  }